﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace GerbilLib
{
    /// <summary>
    /// Provides a wrapper class for the collection of IMDb movie and tv show ratings
    /// published by IMDb in offline text file format.
    /// </summary>
    public class ImdbRating : IMediaItem
    {
        #region .   Private Static Members   .
        /// <summary>
        /// Regular expression for a line in the ratings.list file.
        /// Example:       00.0111101      16   6.2  "$#*! My Dad Says" (2010) {Code Ed (#1.4)}
        /// </summary>
        private static Regex ratingsRegex = new Regex(@"\s+(?<Distribution>.{10})\s+(?<Votes>\d+)\s+(?<Rank>\d+.\d+)\s+(?<Title>.*)");
        #endregion

        #region .   Public Static Members   .
        /// <summary>
        /// Default file name of ratings file
        /// </summary>
        public static readonly string ImdbRatingsFilename = "ratings.list";
        /// <summary>
        /// Occurs when progress is made when loading or processing ratings.
        /// </summary>
        public static event EventHandler<ProgressEventArgs> ProgressEvent;
        #endregion

        #region .   IMediaItem Interface Properties   .
        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>
        /// The title.
        /// </value>
        public string Title { get; set; }
        /// <summary>
        /// Gets or sets the production year.
        /// </summary>
        /// <value>
        /// The production year.
        /// </value>
        public int Year { get; set; }
        /// <summary>
        /// Gets or sets the sort weight.
        /// </summary>
        /// <value>
        /// The sort weight.
        /// </value>
        public int SortWeight { get; set; }
        #endregion

        #region .   Public Properties   .
        /// <summary>
        /// Gets or sets the IMDb rating.
        /// </summary>
        /// <value>
        /// The IMDb rating for this title.
        /// </value>
        public Single Rating { get; set; }
        /// <summary>
        /// Gets or sets the number of votes.
        /// </summary>
        /// <value>
        /// The number of votes.
        /// </value>
        public int Votes { get; set; }
        /// <summary>
        /// Gets or sets the ratings distribution. See the ratings.list file for further
        /// information on what the distribution means and how it is calculated by IMDb.
        /// </summary>
        /// <value>
        /// The ratings distribution.
        /// </value>
        public string Distribution { get; set; }
        #endregion

        #region .   Public Methods   .
        /// <summary>
        /// Prints out the title string of the MediaItem.
        /// </summary>
        /// <returns>Title string of the MediaItem</returns>
        public override string ToString()
        {
            return Title;
        }

        /// <summary>
        /// Checks whether the ratings file already exists or needs to be downloaded first.
        /// </summary>
        /// <returns>True if the file exists, otherwise false.</returns>
        public static bool FileExists()
        {
            return File.Exists(ImdbRatingsFilename);
        }

        /// <summary>
        /// Loads all the IMDb Movie Ratings.
        /// </summary>
        /// <returns>List of IMDb Movie ratings.</returns>
        public static Dictionary<string, HashSet<IMediaItem>> LoadImdbMovieRatings()
        {
            return LoadImdbRatings(loadMovies: true);
        }

        /// <summary>
        /// Loads all the IMDb TV Series Ratings.
        /// </summary>
        /// <returns>List of IMDb TV Series ratings.</returns>
        public static Dictionary<string, HashSet<IMediaItem>> LoadImdbSeriesRatings()
        {
            return LoadImdbRatings(loadSeries: true);
        }

        /// <summary>
        /// Loads all the IMDb Video Game Ratings.
        /// </summary>
        /// <returns>List of IMDb Video Game ratings.</returns>
        public static Dictionary<string, HashSet<IMediaItem>> LoadImdbVideoGameRatings()
        {
            return LoadImdbRatings(loadVideoGames: true);
        }

        /// <summary>
        /// Loads the imdb ratings.
        /// </summary>
        /// <param name="loadMovies">Loads the movie ratings only.</param>
        /// <param name="loadSeries">Loads the tv series ratings only.</param>
        /// <param name="loadVideoGames">Loads the video game ratings only.</param>
        /// <returns>HashSet of media items containing the ratings information.</returns>
        private static Dictionary<string, HashSet<IMediaItem>> LoadImdbRatings(bool loadMovies = false, bool loadSeries = false, bool loadVideoGames = false)
        {
            if (loadMovies)
                Logger.LogMessage("Loading movie titles and their IMDb ratings information...");
            else
                Logger.LogMessage("Loading tv series titles and their IMDb ratings information...");
            System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
            Dictionary<string, HashSet<IMediaItem>> orderedRatings = new Dictionary<string, HashSet<IMediaItem>>();

            stopwatch.Start();
            UpdateProgress(null, 0);
            System.IO.FileInfo fi = new System.IO.FileInfo(ImdbRatingsFilename);
            long length = fi.Length, processed = 0;
            int percentage = 0, newPercentage = 0;
            ImdbRating m = null;
            string line = string.Empty;
            using (System.IO.StreamReader sr = new System.IO.StreamReader(ImdbRatingsFilename))
            {

                IMediaItemEqualityComparer comparer = new IMediaItemEqualityComparer();

                while (!sr.EndOfStream)
                {
                    line = sr.ReadLine();
                    m = GetImdbRating(line, loadMovies, loadSeries, loadVideoGames);
                    if (m != null && !string.IsNullOrEmpty(m.Title))
                    {
                        string cleanTitle = MediaItem.GetCleanTitle(m.Title);
                        HashSet<IMediaItem> ratings = null;
                        if (!orderedRatings.ContainsKey(cleanTitle))
                        {
                            ratings = new HashSet<IMediaItem>();
                            ratings.Add(m);
                            orderedRatings.Add(cleanTitle, ratings);
                        }
                        else
                        {
                            ratings = orderedRatings[cleanTitle];
                            if (!ratings.Contains(m, comparer)) ratings.Add(m);
                        }
                    }
                    processed += line.Length;
                    newPercentage = (int)(processed * 100 / length);
                    if (newPercentage != percentage)
                    {
                        percentage = newPercentage;
                        UpdateProgress(m, percentage);
                    }
                }
            }

            UpdateProgress(null, 100);
            stopwatch.Stop();
            Logger.LogMessage(string.Format("Loaded {0} {1} titles and their IMDB ratings information in {2:F} seconds!", orderedRatings.Count, (loadMovies) ? "movie" : "tv series", stopwatch.ElapsedMilliseconds / 1000.0));

            return orderedRatings;
        }

        /// <summary>
        /// Creates an ImdbRating object from the ratings string from the text file.
        /// </summary>
        /// <param name="input">The input string from the text file.</param>
        /// <param name="returnMovies">if set to <c>true</c> returns movies, if <c>false</c> discard movies.</param>
        /// <param name="returnSeries">if set to <c>true</c> returns tv series, if <c>false</c> discard series.</param>
        /// <param name="returnVideoGames">if set to <c>true</c> returns video games, if <c>false</c> discard video games.</param>
        /// <returns>ImdbRating object with properties populated from input string</returns>
        public static ImdbRating GetImdbRating(string input, bool returnMovies = true, bool returnSeries = false, bool returnVideoGames = false)
        {
            GroupCollection gc = ratingsRegex.Match(input).Groups;
            if (string.IsNullOrEmpty(gc["Title"].Value)) return null;
            // Skip all tv series episode titles, we don't use that level of granularity right now
            if (gc["Title"].Value.Contains("#")) return null;
            if (!returnSeries && gc["Title"].Value.StartsWith("\"")) return null;
            if (!returnVideoGames && gc["Title"].Value.EndsWith("(VG)")) return null;
            if (!returnMovies && !(gc["Title"].Value.StartsWith("\"") || gc["Title"].Value.EndsWith("(VG)"))) return null;

            ImdbRating i = new ImdbRating();
            i.Title = gc["Title"].Value;
            i.Rating = Single.Parse(gc["Rank"].Value);
            i.Votes = int.Parse(gc["Votes"].Value);
            i.Distribution = gc["Distribution"].Value;

            // Get year for advanced title match sorting
            GroupCollection gc2 = MediaItem.RegexLastYear.Match(input).Groups;
            int year = 0;
            int.TryParse(gc2[0].Value, out year);
            i.Year = year;
            
            return i;
        }

        /// <summary>
        /// Fires the progress update event to notify any listener about changes in progress.
        /// </summary>
        /// <param name="rating">The ImdbRating object being processed.</param>
        /// <param name="percentage">The progress percentage.</param>
        public static void UpdateProgress(ImdbRating rating, int percentage)
        {
            if (ProgressEvent != null) ProgressEvent(rating, new ProgressEventArgs(percentage));
        }
        #endregion
    }
}
